using System;
using System.Diagnostics;
using System.Linq;
using System.Collections.Generic;
using LightPersist.Exceptions;
using LightPersist.Transactions;

namespace LightPersist
{

	#region Enumerations

	enum ProcessAction
	{
		Unknown,
		Do,
		Undo,
	}

	enum ProcessDirrection
	{
		Unknown,
		Forward,
		Backward,
	}

	[Flags]
	enum ProcessFlags
	{
		None = 0,
		UpdateStorage = 1,
		UpdateUI = 2,
		UpdateModel = 4,
		CloseTransactions = 8,
		// used only in commit/rollback (not in undo/redo) to change transaction state during processing
	}

	#endregion

	class TransactionProcessor
	{
		readonly LightPersistContext _context;
		readonly Stack<UndoHistoryEntry> _historyRedo = new Stack<UndoHistoryEntry>();
		int commitSeed;

		public TransactionProcessor(LightPersistContext context)
		{
			_context = context;
			HistoryUndo = new Stack<UndoHistoryEntry>();
		}

		public bool CanUndo
		{
			get { return HistoryUndo.Count > 0; }
		}

		public bool CanRedo
		{
			get { return HistoryRedo.Count > 0; }
		}

		internal LightPersistContext LightPersistContext
		{
			get { return _context; }
		}

		Stack<UndoHistoryEntry> HistoryRedo
		{
			get { return _historyRedo; }
		}

		Stack<UndoHistoryEntry> HistoryUndo { get; set; }

		internal bool SkipCheckingAndLoggingTransaction { get; private set; }

		internal bool NotifyingInProgress { get; private set; }

		internal void PerformUndo()
		{
			var lastStory = HistoryUndo.Pop();
			HistoryRedo.Push(lastStory);
			PerformUndo(lastStory.Transaction);
			NotifyContextUndoRedoProperties();
		}

		internal void PerformRedo()
		{
			var story = HistoryRedo.Pop();
			HistoryUndo.Push(story);
			PerformRedo(story.Transaction);
			NotifyContextUndoRedoProperties();
		}

		void NotifyContextUndoRedoProperties()
		{
			LightPersistContext.OnPropertyChanged("CanUndo");
			LightPersistContext.OnPropertyChanged("CanRedo");
		}

		void PerformRedo(Transaction transaction)
		{
			try
			{
				SkipCheckingAndLoggingTransaction = true;
				ProcessTransactionForward(transaction, ProcessFlags.UpdateStorage | ProcessFlags.UpdateUI | ProcessFlags.UpdateModel);
			}
			finally
			{
				SkipCheckingAndLoggingTransaction = false;
			}
		}

		internal void AddStory(Transaction transaction)
		{
			HistoryRedo.Clear();
			HistoryUndo.Push(new UndoHistoryEntry { Transaction = transaction, });

			if (HistoryUndo.Count > 100 && ++commitSeed % 32 == 0)
			{
				// Debug.WriteLine("Trim undo history: cnt={0}, seed={1}".Arg(HistoryUndo.Count, commitSeed % 32));
				HistoryUndo = new Stack<UndoHistoryEntry>(HistoryUndo.Take(100));
			}

			NotifyContextUndoRedoProperties();
		}

		/// <summary>
		/// CommitTransaction (Not Redo!!)
		/// </summary>
		/// <param name="transaction"></param>
		internal void CommitTransaction(Transaction transaction)
		{
#if DEBUG
			if (transaction == null)
			{
				throw new ArgumentNullException("transaction");
			}

			if (transaction.Parent != null)
			{
				throw new ArgumentException("Only root transaction can be passed to the Commit. Because it is impossible to process transaction by parts or to add part of transaction into undo history");
			}

			if (transaction.State != TransactionState.Started)
			{
				throw new TransactionException("CommitTransaction: this transaction is not in the stated state. State = " + transaction.State);
			}

#endif

			try
			{
				// VerifyBeforeCommit();

				ProcessTransactionForward(transaction, ProcessFlags.UpdateStorage | ProcessFlags.UpdateUI | ProcessFlags.CloseTransactions);
				if (transaction.Actions.Any())
				{
					AddStory(transaction);
				}
			}
			catch
			{
				transaction.State = TransactionState.Error;
				throw;
			}
		}

//		private void VerifyBeforeCommit()
//		{
//			foreach (var item in LightPersistContext.DbConfig.NewObjectsHolder)
//			{
//				// only DbConfig.NewObjectsHolder reference this object?
//				if (item.StrongBackReferences.Count <= 1)
//				{
//					throw new LightPersistException(
//						@"
//New object '{0}' was not referenced by any strong reference.
//Every object should have at least one strong reference path from Root Object.
//ID = {1}
//"
//							.Arg(item.RealType.Name, item.ID));
//				}
//			}
//			LightPersistContext.DbConfig.NewObjectsHolder.Clear();
//		}

		void ProcessTransactionForward(Transaction tr, ProcessFlags flags)
		{
			var trs = tr.State;
			try
			{
				tr.State = TransactionState.Processing;
				ProcessTransaction(tr, ProcessDirrection.Forward, ProcessAction.Do, flags);
			}
			finally
			{
				if (tr.State == TransactionState.Processing)
				{
					tr.State = trs;
				}
			}
		}

		void ProcessTransactionBackward(Transaction tr, ProcessFlags flags)
		{
			ProcessTransaction(tr, ProcessDirrection.Backward, ProcessAction.Undo, flags);
		}

		void ProcessTransaction(Transaction transaction, ProcessDirrection dirrection, ProcessAction actionDoOrUndo, ProcessFlags flags_)
		{
#if DEBUG
			if (transaction == null)
			{
				throw new ArgumentNullException("transaction");
			}
			//			if(transaction.State==TransactionState.Error)
			//			{
			//				throw new InvalidOperationException("Processing transaction marked with error state");
			//			}
#endif

			if (transaction.SkipDuringProcess)
			{
				return;
			}

			var ctx = new TransactionProcessingContext(this, flags_, LightPersistContext);

			var indention = transaction.LoggingIndention;

			Debug.WriteLine(string.Format(indention + "<ProcessTransaction {3} Flags='{0}, {1}, {2}' >", dirrection, actionDoOrUndo, ctx.Flags, transaction));

			var actions = transaction.Actions;
			var actionsCount = actions.Count;

			int startIndex, stopIndex, indexIncrement;
			DefineCycleVars(dirrection, actionsCount, out startIndex, out stopIndex, out indexIncrement);

			//var skip_checking_and_logging_transaction_before = SkipCheckingAndLoggingTransaction;

			// when transaction are broken on the middle
			//			var actionNumberForRollingBackHalflyCommitedTransaction = -1;
			//
			//			if(transaction.ProcessingStoppedSnapExist)
			//			{
			//				Debug.Assert(transaction.ProcessingStoppedAt >= 0);
			//				Debug.Assert(transaction.State == TransactionState.Error);
			//				Debug.Assert(ctx.NeedCloseTransactions);
			//				Debug.Assert(dirrection == ProcessDirrection.Backward);
			//				actionNumberForRollingBackHalflyCommitedTransaction = transaction.ProcessingStoppedAt;
			//			}

			// check integrity

			// main dispatching loop
			//Exception exception = null;
			for (var i = startIndex; i != stopIndex; i += indexIncrement)
			{
				try
				{
					var actionEntry = actions[i];
					Debug.WriteLine(transaction.LoggingIndention + (actionDoOrUndo == ProcessAction.Undo ? "UNDO " : null) + actionEntry.AsAction);

					//					if(actionNumberForRollingBackHalflyCommitedTransaction==i)
					//					{
					//						ctx.Flags |= transaction.ProcessingStoppedWithFlags;
					//					}

					if (!actionEntry.IsTransaction)
					{
						var action = actionEntry.AsAction;
						switch (actionDoOrUndo)
						{
							case ProcessAction.Do:
								action.ProcessDo(ctx);
								action.ProcessFlags |= ctx.Flags;
								break;
							case ProcessAction.Undo:
								ProcessFlags flagsBefore = 0;
								// Rolbacking of commited actions
								if (action.ProcessFlags != 0)
								{
									flagsBefore = ctx.Flags;
									ctx.Flags |= action.ProcessFlags;
								}
								action.ProcessUndo(ctx);
								if (action.ProcessFlags != 0)
								{
									ctx.Flags = flagsBefore;
									action.ProcessFlags = 0;
								}
								break;
							default:
								throw new ArgumentException("Invalid ProcessAction: " + actionDoOrUndo);
						}
						action.TargetModel.IsReadonly = true;
					}
					else
					{
						Debug.WriteLine(indention + "Nested transaction:");
						ProcessTransaction(actionEntry.AsTransaction, dirrection, actionDoOrUndo, ctx.Flags);
					}
				}
				catch (Exception ex)
				{
					//					if(exception!=null)
					//					{
					//						throw new LightPersistException("Critical rollback exception!! Consistency was corrupted during commit, and autorollback of current transaction was performed, but some exception was thrown during auto rollback");
					//					}

					if (transaction.State == TransactionState.Error)
					{
						throw new LightPersistException("Transaction already marked as error. It is strange, some error information will be missed", ex);
					}

					// only on commit can be reverted to rollback
					if (!ctx.NeedCloseTransactions || actionDoOrUndo == ProcessAction.Undo)
					{
						throw new CriticalInternalLightPersistException("Processing of closed transaction or rollbacking produces exception!! It is very critical. Undo/Redo/Rollback should not corrupt consistency.", ex);
					}

					// transaction.State = TransactionState.Error;
					// transaction.SnapProcessingStateOnCommitFailed(i, ctx.Flags);
					throw;
					// exception = ex;

					// reverse transaction processing
					//					Trace.WriteLine("!!Transaction processing error: "+ex);
					//					actionDoOrUndo = ProcessAction.Undo;
					//					dirrection = ProcessDirrection.Backward;
					// ctx.Flags |= ProcessFlags.UpdateModel;
					// SkipCheckingAndLoggingTransaction = true;
					//					Trace.WriteLine("!!Reverse processing dirrection to: " + dirrection);
					//					DefineCycleVars(dirrection, actionsCount, out startIndex, out stopIndex, out indexIncrement);
				}
			}

			// if(exception != null)
			// {
			// //SkipCheckingAndLoggingTransaction = skip_checking_and_logging_transaction_before;
			// transaction.State = TransactionState.Error;
			// throw exception;
			// }

			// notify ui simple properties - separate cicle
			if (ctx.NeedUpdateComponents)
			{
				try
				{
					NotifyingInProgress = true;
					foreach (var action in ctx.ActionsList)
					{
								//action.List.Notify(action.Item, action.Index, actionDoOrUndo == ProcessAction.Undo ^ action.IsInserting );
								action.List.Notify(action.Item, action.Index, action.IsInserting);
						}
					
					foreach (var entry in ctx.MapTargetToAction.Values)
					{
						if (!Equals(entry.InitialValue, entry.LastValue))
						{
							entry.LastAction.TargetModel.OnPropertyChanged(entry.LastAction.PropertyName);
						}
					}
				}
				finally
				{
					NotifyingInProgress = false;
				}
			}

			// Closing transactions. First time only... not for undo/redo
			if (ctx.NeedCloseTransactions)
			{
				switch (actionDoOrUndo)
				{
					case ProcessAction.Do:
						transaction.State = TransactionState.Commited;
						break;
					case ProcessAction.Undo:
						transaction.State = TransactionState.RolledBack;
						transaction.SkipDuringProcess = true;
						break;
					default:
						throw new ArgumentException("Invalid ProcessAction: " + actionDoOrUndo);
				}
			}

			Debug.WriteLine(indention + "</ProcessTransaction>");
		}

		static void DefineCycleVars(ProcessDirrection dirrection, int actionsCount, out int startIndex, out int stopIndex, out int indexIncrement)
		{
			switch (dirrection)
			{
				case ProcessDirrection.Forward:
					indexIncrement = 1;
					startIndex = 0;
					stopIndex = actionsCount;
					break;
				case ProcessDirrection.Backward:
					indexIncrement = -1;
					startIndex = actionsCount - 1;
					stopIndex = -1;
					break;
				default:
					throw new ArgumentException("Invalid ProcessDirrection: " + dirrection);
			}
		}

		void PerformUndo(Transaction tr)
		{
			try
			{
				SkipCheckingAndLoggingTransaction = true;
				ProcessTransactionBackward(tr, ProcessFlags.UpdateStorage | ProcessFlags.UpdateUI | ProcessFlags.UpdateModel);
			}
			finally
			{
				SkipCheckingAndLoggingTransaction = false;
			}
		}

		/// <summary>
		/// Rollback (Not Undo!!)
		/// </summary>
		/// <param name="transaction"></param>
		internal void RollbackTransaction(Transaction transaction)
		{
#if DEBUG
			if (transaction == null)
			{
				throw new ArgumentNullException("transaction");
			}
			if (!transaction.State.CanRollback())
			{
				throw new TransactionException("RollbackTransaction: specified transaction is not in started state. State = " + transaction.State);
			}
#endif

			Debug.WriteLine(transaction.LoggingIndention + "!Rollback! " + transaction);

			try
			{
				SkipCheckingAndLoggingTransaction = true;

				var additional = default(ProcessFlags);
				//				if(transaction.State==TransactionState.Error)
				//				{
				//#warning //TODO write test for extra notifying of lists. The bug is here. When some list action are performed after creation cycle, rollbacking perform reverse action, even if dirrect action was not processed and notified (due to cycle detection)
				//					additional |= ProcessFlags.UpdateStorage | ProcessFlags.UpdateUI;
				//				}

				ProcessTransactionBackward(transaction, ProcessFlags.UpdateModel | ProcessFlags.CloseTransactions | additional);
			}
			catch (Exception ex)
			{
				Trace.WriteLine(ex.ToString());
				throw;
			}
			finally
			{
				SkipCheckingAndLoggingTransaction = false;
				Debug.Assert(transaction.State == TransactionState.RolledBack);
				Debug.Assert(transaction.SkipDuringProcess);
			}
		}

//		internal void ProcessPropertyChangedAction(PropertyChangedAction action, TransactionProcessingContext ctx)
//		{
//			if (ctx.NeedUpdateModel)
//			{
//				action.TargetModel.ObjectState.Set(action.PropertyName, action.NewValue);
//			}
//			if (ctx.NeedUpdateComponents)
//			{
//				ctx.ScheduleNotifyPropertyValue(action, action.OldValue, action.NewValue);
//				//				var propInfo = action.TargetModel.ObjectState.ClassMetainfo.GetPropertyInfo(action.PropertyName);
//				//				// Notify only known public properties, e.g. '__BackReferences' are excluded
//				//				if(propInfo != null)
//				//				{
//				//					}
//				//				}
//			}
//
//			if (ctx.NeedUpdateStorage)
//			{
//				if (LightPersistContext.Storage.IsExists(action.TargetModel.ObjectState))
//				{
//					LightPersistContext.Storage.UpdateObject(action.TargetModel.ObjectState);
//				}
//			}
//		}
//
//		internal void ProcessPropertyChangedActionUndo(PropertyChangedAction action, TransactionProcessingContext ctx)
//		{
//			if (ctx.NeedUpdateModel)
//			{
//				action.TargetModel.ObjectState.Set(action.PropertyName, action.OldValue);
//			}
//
//			if (ctx.NeedUpdateStorage)
//			{
//				// update properties only for existing objects
//				// newly created objects would be added into storage later
//				if (LightPersistContext.Storage.IsExists(action.TargetModel.ObjectState))
//				{
//					LightPersistContext.Storage.UpdateObject(action.TargetModel.ObjectState);
//				}
//			}
//
//			if (ctx.NeedUpdateComponents)
//			{
//				ctx.ScheduleNotifyPropertyValue(action, action.NewValue, action.OldValue);
//			}
//		}

//		internal void ProcessListPropertyChangedAction(ListPropertyChangedAction action, TransactionProcessingContext ctx)
//		{
//			if (!action.Validated)
//			{
//				DagIntegrityChecker.CheckIntegrity(action.TargetModel);
//				action.Validated = true;
//			}
//
//			if (ctx.NeedUpdateModel)
//			{
//				action.TargetModel.ObjectState.Set(action.PropertyName, action.NewValue);
//			}
//
//			if (ctx.NeedUpdateStorage)
//			{
//				LightPersistContext.Storage.UpdateObject(action.TargetModel.ObjectState);
//			}
//
//			if (ctx.NeedUpdateComponents)
//			{
//				//ctx.AddToNotify(action);
//				var propInfo = action.TargetModel.ObjectState.ClassMetainfo.GetPropertyInfo(action.PropertyName);
//				// Notify only known public properties, e.g. '__BackReferences' are excluded
//				if (propInfo != null)
//				{
//					// is this is public collection or it is public singleitem collection
//					if (propInfo.IsList)
//					{
//						ctx.AddToNotifyActionList(action);
//						//						var listCore = action.LightListCore;
//						//						// the proxy exists only if anyone are accessed this list externally, and possible, subscribed
//						//						if(listCore.LightListProxyInternal != null)
//						//						{
//						////							var proxyInternal = listCore.LightListProxyInternal;
//						////							Trace.TraceInformation("!!! Notify Collection Changed");
//						////							proxyInternal.OnCollectionChanged(NotifyCollectionArgsFactory.Create(action, false));
//						//						}
//					}
//					else
//					{
//						ctx.ScheduleNotifyPropertyValue(action, action.OldValue, action.NewValue);
//					}
//				}
//			}
//		}
//
//		// TODO: remove a lot of duplication on processing functions!
//		internal void ProcessListPropertyChangedActionUndo(ListPropertyChangedAction action, TransactionProcessingContext ctx)
//		{
//			if (ctx.NeedUpdateModel)
//			{
//				action.TargetModel.ObjectState.Set(action.PropertyName, action.OldValue);
//			}
//
//			if (ctx.NeedUpdateStorage)
//			{
//				LightPersistContext.Storage.UpdateObject(action.TargetModel.ObjectState);
//			}
//
//			if (ctx.NeedUpdateComponents)
//			{
//				//ctx.AddToNotify(action);
//				var propInfo = action.TargetModel.ObjectState.ClassMetainfo.GetPropertyInfo(action.PropertyName);
//				// Notify only known public properties, e.g. '__BackReferences' are excluded
//				if (propInfo != null)
//				{
//					// is this is public collection or it is public singleitem collection
//					if (propInfo.IsList)
//					{
//						ctx.AddToNotifyActionList(action);
//						//						var listCore = action.LightListCore;
//						//						// the proxy exists only if anyone are accessed this list externally, and possible, subscribed
//						//						if(listCore.LightListProxyInternal != null && (action.ProcessFlags&ProcessFlags.UpdateUI)==0)
//						//						{
//						//							//var proxyInternal = listCore.LightListProxyInternal;
//						//							try
//						//							{
//						////								NotifyingInProgress = true;
//						////								Trace.TraceInformation("!!! Notify Collection Changed");
//						////								proxyInternal.OnCollectionChanged(NotifyCollectionArgsFactory.Create(action, true));
//						//							}
//						//							finally
//						//							{
//						//								NotifyingInProgress = false;
//						//							}
//						//						}
//					}
//					else
//					{
//						ctx.ScheduleNotifyPropertyValue(action, action.NewValue, action.OldValue);
//					}
//				}
//			}
//		}

		#region Nested type: UndoHistoryEntry

		class UndoHistoryEntry
		{
			public Transaction Transaction;

			public override string ToString()
			{
				return Transaction == null ? "null" : Transaction.ToString();
			}
		}

		#endregion
	}
}